package jmine.tec.script.persistent.services.incluir;

import java.lang.reflect.TypeVariable;
import java.util.Map;
import java.util.Set;

import jmine.tec.database.pack.PackException;
import jmine.tec.database.pack.services.PackEntityPersistenceHelper;
import jmine.tec.di.annotation.Injected;
import jmine.tec.script.persistent.ScriptPersistenceMessages;
import jmine.tec.script.persistent.domain.Script;
import jmine.tec.script.persistent.domain.ScriptDependency;
import jmine.tec.script.persistent.domain.ScriptLanguage;
import jmine.tec.script.persistent.domain.ScriptPurpose;
import jmine.tec.services.annotations.Execution;
import jmine.tec.services.annotations.Input;
import jmine.tec.services.annotations.Output;
import jmine.tec.services.annotations.WithPreffix;
import jmine.tec.services.annotations.WithRegex;
import jmine.tec.services.api.RawInput;
import jmine.tec.services.api.ServiceExecutionException;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

/**
 * Classe abstrata de serviços de inclusão de script
 * 
 * @author lundberg
 * @param <S> tipo de script criado
 */
public abstract class AbstractScriptService<S extends Script> {

    public static final String IDENTIFICADOR = "Identificador";

    public static final String MNEMONICO = "Mnemônico";

    public static final String DESCRICAO = "Descrição";

    public static final String INJECT_TEMPLATE = "Injetar em %s";

    public static final String INJECT_REGEX = "Injetar em (\\w+)";

    public static final String API_PREFIX = "API";

    public static final String TEXTO = "Texto";

    public static final String DATABASE_ID = "Database ID";

    public static final String FINALIDADE = "Finalidade";

    public static final String LINGUAGEM = "Linguagem";

    // Injected
    private PackEntityPersistenceHelper helper;

    private RawInput rawInput;

    // Obrigatórios
    private String mnemonico;

    private ScriptLanguage linguagem;

    private ScriptPurpose finalidade;

    // private String texto;

    // Opcionais
    private Set<Script> apis;

    private Map<String, String> dependencias;

    private Long databaseID;

    /**
     * Inclui um novo script Hydra ao sistema.
     * 
     * @throws ServiceExecutionException ServiceExecutionException
     * @throws PackException PackException
     */
    @Execution
    @Output(propertyName = IDENTIFICADOR)
    public void execute() throws ServiceExecutionException, PackException {
        S script = this.helper.findOrCreate(this.getScriptClass(), this.databaseID, this.mnemonico);
        script.getApis().clear();
        script.getDependencies().clear();
        script.setMnemonico(this.mnemonico);
        script.setDescricao(this.rawInput.getRaw(DESCRICAO));
        script.setLanguage(this.linguagem);
        script.setPurpose(this.finalidade);
        script.getApis().addAll(this.apis);
        script.setBody(this.rawInput.getRaw(TEXTO));
        for (String key : this.dependencias.keySet()) {
            ScriptDependency dependency = new ScriptDependency();
            dependency.setBean(this.rawInput.getRaw(String.format(INJECT_TEMPLATE, key)));
            dependency.setVariable(key);
            script.getDependencies().add(dependency);
        }
        this.fillSpecific(script);
        this.helper.save(script, this.databaseID);
    }

    /**
     * Preenche dados específicos da implementação.
     * 
     * @param script script
     * @throws ServiceExecutionException ServiceExecutionException
     */
    protected abstract void fillSpecific(S script) throws ServiceExecutionException;

    /**
     * Devolve a classe do script a ser criado
     * 
     * @return class
     * @throws ServiceExecutionException ServiceExecutionException
     */
    @SuppressWarnings("unchecked")
    protected Class<S> getScriptClass() throws ServiceExecutionException {
        try {
            TypeVariable typeVariable = AbstractScriptService.class.getTypeParameters()[0];
            return (Class<S>) GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType(typeVariable, this.getClass());
        } catch (IllegalArgumentException e) {
            throw new ServiceExecutionException(ScriptPersistenceMessages.INVALID_SCRIPT_SERVICE_IMPLEMENTATION.create(this.getClass()), e);
        }
    }

    /**
     * @param texto the texto to set
     */
    @Input(fieldName = TEXTO)
    public void setTexto(String texto) {
        // OK
    }

    /**
     * @param mnemonico the mnemonico to set
     */
    @Input(fieldName = MNEMONICO)
    public void setMnemonico(String mnemonico) {
        this.mnemonico = mnemonico;
    }

    /**
     * @param linguagem the linguagem to set
     */
    @Input(fieldName = LINGUAGEM)
    public void setLinguagem(ScriptLanguage linguagem) {
        this.linguagem = linguagem;
    }

    /**
     * @param finalidade the finalidade to set
     */
    @Input(fieldName = FINALIDADE)
    public void setFinalidade(ScriptPurpose finalidade) {
        this.finalidade = finalidade;
    }

    /**
     * @param apis the apis to set
     */
    @WithPreffix(preffix = API_PREFIX)
    public void setApis(Set<Script> apis) {
        this.apis = apis;
    }

    /**
     * @param dependencias the dependencias to set
     */
    @WithRegex(regex = INJECT_REGEX, groupIndex = 1)
    public void setDependencias(Map<String, String> dependencias) {
        this.dependencias = dependencias;
    }

    /**
     * @param databaseID the databaseID to set
     */
    @Input(fieldName = DATABASE_ID, required = false)
    public void setDatabaseID(Long databaseID) {
        this.databaseID = databaseID;
    }

    /**
     * @param descricao the descricao to set
     */
    @Input(fieldName = DESCRICAO, required = false)
    public void setDescricao(String descricao) {
        // Descricao eh coletada RAW direto do serviceProperties
        // Esse metodo esta aqui apenas para documentacao do parametro
    }

    /**
     * @param helper the helper to set
     */
    @Injected
    public void setHelper(PackEntityPersistenceHelper helper) {
        this.helper = helper;
    }

    /**
     * @param rawInput the rawInput to set
     */
    @Injected
    public void setRawInput(RawInput rawInput) {
        this.rawInput = rawInput;
    }
}
